#include <Arduino.h>
#include <HardwareSerial.h>
#include <time.h>
#include <stdint-gcc.h>

#include "Timer.h"
#include "Illumination.h"

Levels::Levels(uint8_t count) : count(count) {
    lines = new uint8_t[count];
    clear();
}

Levels::~Levels() {
    delete[] lines;
}

void Levels::clear() const {
    for (int i = 0; i < count; ++i) {
        lines[i] = 0;
    }
}

const uint8_t Levels::getLevel(uint8_t line) const {
    checkLineNumber(line);
    return lines[line];
}

void Levels::setLevel(uint8_t line, uint8_t level) const {
    checkLineNumber(line);
    checkLevelValue(level);
    lines[line] = level;
}

void Levels::setLevels(uint8_t levels[]) const {
    for (int i = 0; i < count; ++i) {
        lines[i] = levels[i];
    }
}

const uint8_t Levels::getSize() const {
    return count;
}


void Levels::setLevels(uint8_t level) const {
    for (int i = 0; i < count; ++i) {
        lines[i] = level;
    }
}

void Levels::setLevels(Levels &level) const {
    clear();

    uint8_t c = min(getSize(), level.getSize());
    for (uint8_t i = 0; i < c; i++) {
        lines[i] = level.getLevel(i);
    }
}

error_t Levels::checkLineNumber(uint8_t line) const {
    if (line < 0) {
        return ERROR_ILLUMINATION_LINES;
    }
    if (line >= count) {
        return ERROR_ILLUMINATION_LINES;
    }
    return NO_ERROR;
}

error_t Levels::checkLevelValue(uint8_t level) const {
    if (level < MIN_LEVEL_VALUE) {
        return ERROR_ILLUMINATION_LEVELS;
    }
    if (level > MAX_LEVEL_VALUE) {
        return ERROR_ILLUMINATION_LEVELS;
    }
    return NO_ERROR;
}

TimePoint::TimePoint(seconds_t time, uint8_t levels) : time(time), next(NULL), previous(NULL) {
    this->levels = new Levels(levels);
}

TimePoint::~TimePoint() {
    delete levels;
}

const seconds_t TimePoint::getTime() const {
    return time;
}

const Levels *TimePoint::getLevels() const {
    return levels;
}

const TimePoint *TimePoint::getNextPoint() const {
    return next;
}

const TimePoint *TimePoint::getPreviousPoint() const {
    return previous;
}

Illumination::Illumination(uint8_t lines, uint8_t const pins[])
        : lines(lines), points(0), previousSeconds(0), head(NULL), tail(NULL), mode(AUTO) {
    levels = new Levels(lines);

    this->pins = new uint8_t[lines];
    for (int i = 0; i < lines; ++i) {
        this->pins[i] = pins[i];
    }
}

Illumination::~Illumination() {
    delete pins;

    delete levels;

    TimePoint *tp = head;
    while (tp != NULL) {
        TimePoint *tbd = tp;
        tp = tp->next;
        delete tbd;
    }
    head = tail = NULL;
}

Levels const *Illumination::createPoint(seconds_t time) {
    TimePoint *pElement = findElement(time, SAME);
    if (pElement) {
        return pElement->levels;
    }

    TimePoint *element = new TimePoint(time, lines);

    if (!head) {
        head = tail = element;
    } else {
        TimePoint *pLevelElement = findElement(time, BEFORE);
        if (!pLevelElement) { // head
            element->next = head;
            head->previous = element;
            head = element;
        } else {
            element->next = pLevelElement->next;
            element->previous = pLevelElement;

            pLevelElement->next = element;

            if (pLevelElement == tail) { // tail
                tail = element;
            } else {
                element->next->previous = element;
            }
        }
    }
    points++;
    return element->getLevels();
}

void Illumination::deletePoint(seconds_t time) {
    TimePoint *pElement = findElement(time, SAME);
    if (!pElement) {
        return;
    }

    if (pElement->previous) {
        pElement->previous->next = pElement->next;
    }

    if (pElement->next) {
        pElement->next->previous = pElement->previous;
    }
    delete pElement;
}

const Mode Illumination::getMode() const {
    return mode;
}

void Illumination::setMode(const Mode mode) {
    this->mode = mode;

    if (mode == BLAZE) { // for blaze set all to 255
        levels->setLevels(255);
        validatePins();
    }
}

const Levels *Illumination::getLevels() const {
    return levels;
}

const uint8_t Illumination::getSize() const {
    return points;
}


const TimePoint *Illumination::getFirstPoint() const {
    return head;
}

const TimePoint *Illumination::getLastPoint() const {
    return tail;
}

TimePoint *Illumination::findElement(seconds_t point, SearchOption option) {
    TimePoint *ce = NULL;
    if (option == SAME) {
        ce = head;
        while (ce && ce->time != point) {
            ce = ce->next;
        }
    } else if (option == BEFORE) {
        ce = tail;
        while (ce && ce->time >= point) {
            ce = ce->previous;
        }
    } else if (option == AFTER) {
        ce = head;
        while (ce && ce->time <= point) {
            ce = ce->next;
        }
    }
    return ce;
}

void Illumination::validatePins() {
    for (uint8_t i = 0; i < levels->getSize(); ++i) {
        analogWrite(pins[i], levels->getLevel(i));
    }
}

bool Illumination::calculateLevel(time_t time, Levels *levels) {
    bool update = false;
    if (head == tail) {
        levels->clear();
    } else {
        seconds_t seconds = elapsedSecsToday(time);
        long fromT, toT;
        TimePoint *fromEl, *toEl;
        TimePoint *pElement = findElement(seconds, BEFORE);
        if (!pElement) { // before head
            fromT = tail->time - SECS_PER_DAY;
            toT = head->time;
            fromEl = tail;
            toEl = head;
        } else if (!pElement->next) { // after tail
            fromT = pElement->time;
            toT = head->time + SECS_PER_DAY;
            fromEl = pElement;
            toEl = head;
        } else {
            fromT = pElement->time;
            toT = pElement->next->time;
            fromEl = pElement;
            toEl = pElement->next;
        }

        for (uint8_t i = 0; i < lines; ++i) {
            const uint8_t level = (uint8_t) map(seconds, fromT, toT, fromEl->levels->getLevel(i), toEl->levels->getLevel(i));
            if (levels->getLevel(i) != level) {
                update = true;
            }
            levels->setLevel(i, level);
        }
    }
    return update;
}

const error_t Illumination::tick(const tick_t ticks, const time_t time) {
    if (mode == MANUAL || mode == BLAZE) {
        // do nothing
    } else if (mode == LIGHTNING) {
        // TBD: Lightning code is under development
    } else {
        time_t t = time;
        if (mode == DEMO) {
            t *= 100;
        }
        const int currentSeconds = (int const) numberOfSeconds(t);
        if (currentSeconds != previousSeconds) {
            if (calculateLevel(t, levels)) {
                validatePins();
                lastUpdateTime = t;
            }
            previousSeconds = currentSeconds;
        }
    }
    return NO_ERROR;
}

const time_t Illumination::getLastUpdateTime() const {
    return lastUpdateTime;
}

const error_t Illumination::begin() {
    createTimePoint(8, 0, 0, 0, 255);
    createTimePoint(9, 10, 10, 10, 10);
    createTimePoint(10, 180, 180, 180, 0);
    createTimePoint(14, 255, 255, 255, 0);
    createTimePoint(16, 255, 255, 255, 0);
    createTimePoint(19, 30, 30, 140, 0);
    createTimePoint(20, 0, 0, 0, 0);
    createTimePoint(22, 0, 0, 0, 255);

    return NO_ERROR;
}

void Illumination::createTimePoint(uint8_t hour, uint8_t s1, uint8_t s2, uint8_t ill, uint8_t moon) {
    uint8_t t1[] = {s1, s2, ill, moon};
    createPoint(timeToSeconds(hour, 0, 0))->setLevels(t1);
}

const bool Illumination::isSupported(String const &request) {
    return request.startsWith("ILL");
}

const error_t Illumination::processCommand(String const &request, String &response) {
    if (request.equals("ILL+LINES?")) {
        response = response + lines;
    } else if (request.startsWith("ILL+CURRENT")) {
        if (request.indexOf("=") > 0) {
            int pos = 0;
            uint8_t i = 0;

            while (true) {
                int p = request.indexOf(":", (unsigned int) pos);
                if (p < 0) {
                    break;
                }
                levels->setLevel(i++, (uint8_t) request.substring((unsigned int) pos, (unsigned int) p).toInt());
                pos = p + 1;
            }
            validatePins();
        }

        response = "";
        uint8_t size = levels->getSize();
        for (uint8_t i = 0; i < size; ++i) {
            if (i != 0) {
                response += ":";
            }
            response += levels->getLevel(i);
        }
    } else if (request.startsWith("ILL+MODE")) {
        if (request.indexOf("=") > 0) {
            if (request.endsWith("AUTO")) {
                setMode(AUTO);
            } else if (request.endsWith("DEMO")) {
                setMode(DEMO);
            } else if (request.endsWith("BLAZE")) {
                setMode(BLAZE);
            } else if (request.endsWith("MANUAL")) {
                setMode(MANUAL);
            } else if (request.endsWith("LIGHTNING")) {
                setMode(LIGHTNING);
            }
        }
        response = (mode == AUTO ? "AUTO" : (mode == DEMO ? "DEMO" : (mode == MANUAL ? "MANUAL" : (mode == LIGHTNING ? "LIGHTNING" : (mode == BLAZE ? "BLAZE" : "UNDEFINED")))));
    }
    return NO_ERROR;
}